package org.shunya.duplicatefileperquisitor.model;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.util.zip.CRC32;

public class File implements Comparable, Cloneable {
    public static final double THRESHOLD_VALUE = 0.25;
    private final Path path;
    private final FileName fileName;
    private final FileType fileType;
    private SimpleBooleanProperty selected = new SimpleBooleanProperty(false);
    private Long crcValue = null;
    private Integer group = 0;
    private boolean originalFile = false;

    public File(Path path) {
        this.path = path;
        this.fileName = FileName.fileName(path.getFileName().toString());
        String[] strings = path.toString().split(".");
        if (strings.length == 0) {
            this.fileType = FileType.UNKNOWN;
        } else {
            this.fileType = FileType.getFileType(strings[strings.length - 1]);
        }
    }

    public Path getPath() {
        return path;
    }

    public File getFile() {
        return this;
    }

    public String getFileName() {
        return fileName.getFileName();
    }

    public String getFilePath() {
        return path.toFile().getAbsolutePath();
    }

    public FileType getFileType() {
        return fileType;
    }

    public BooleanProperty selectedProperty() {
        return selected;
    }

    public boolean isSelected() {
        return selected.get();
    }

    public void setSelected(boolean selected) {
        this.selected.set(selected);
    }

    public boolean isOriginalFile() {
        return originalFile;
    }

    public void setOriginalFile(boolean originalFile) {
        this.originalFile = originalFile;
    }

    public Integer getGroup() {
        return group;
    }

    public void setGroup(Integer group) {
        this.group = group;
    }

    public Long getCRC32Value() {
        return crcValue == null ? calculateCRC() : crcValue;
    }

    private Long calculateCRC() {
        final int SIZE = 16 * 1024;
        final int SIZE2 = 16 * 1024 * 1024 * 100;
        try (FileInputStream in = new FileInputStream(path.toFile())) {
            CRC32 crc = new CRC32();
            FileChannel channel = in.getChannel();
            long length = channel.size();
            long iterations = length / SIZE2;
            long reverseLength = 0l;
            for (int i = 0; i <= iterations; i++) {
                MappedByteBuffer mb = channel.map(FileChannel.MapMode.READ_ONLY, reverseLength, i == iterations ? length % SIZE2 : SIZE2);
                reverseLength += SIZE2;
                byte[] bytes = new byte[SIZE];
                int nGet = 0;
                while (mb.hasRemaining()) {
                    nGet = Math.min(mb.remaining(), SIZE);
                    mb.get(bytes, 0, nGet);
                    crc.update(bytes, 0, nGet);
                }
            }
            crcValue = crc.getValue();
            return crcValue;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new RuntimeException("unknown IO error occurred ");
    }

    public boolean isSimilar(File other) {
        final int SIZE = 16 * 1024;
        byte[] thisFileChunk = new byte[SIZE];
        byte[] otherFileChunk = new byte[SIZE];

        int thisFileHashCode = 0;
        int otherFileHashcode = 0;
        try (FileInputStream inA = new FileInputStream(this.getPath().toFile()); FileInputStream inB = new FileInputStream(other.getPath().toFile());) {
            FileChannel channel1 = inA.getChannel();
            MappedByteBuffer mb1 = channel1.map(FileChannel.MapMode.READ_ONLY, 0, (int) channel1.size());
            FileChannel channel2 = inB.getChannel();
            MappedByteBuffer mb2 = channel2.map(FileChannel.MapMode.READ_ONLY, 0, (int) channel2.size());
            boolean mb1b;
            boolean mb2b = false;
            while ((mb1b = mb1.hasRemaining()) || (mb2b = mb2.hasRemaining())) {
                if (mb1b) {
                    mb1.get(thisFileChunk, 0, Math.min(mb1.remaining(), SIZE));
                    thisFileHashCode += customHashCode(thisFileChunk);
                }
                if (mb2b) {
                    mb2.get(otherFileChunk, 0, Math.min(mb2.remaining(), SIZE));
                    otherFileHashcode += customHashCode(otherFileChunk);
                }
            }
            float i = (float) (thisFileHashCode - otherFileHashcode) / thisFileHashCode;
            if (Math.abs(i) <= THRESHOLD_VALUE) {
                return true;
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        } catch (IOException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
        return false;
    }

    private int customHashCode(byte a[]) {
        if (a == null)
            return 0;

        int result = 0;
        for (byte element : a)
            result = result + element;

        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        File file = (File) o;

        if (fileName != null ? !fileName.equals(file.fileName) : file.fileName != null) return false;
        if (fileType != file.fileType) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = fileName != null ? fileName.hashCode() : 0;
        result = 31 * result + (fileType != null ? fileType.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return path.toFile().getAbsolutePath();
    }

    @Override
    public int compareTo(Object o) {
        if(o instanceof File) {
            return getGroup().compareTo(((File) o).getGroup());
        }
        return -1;
    }
}
